package org.amse.korovkin.ciphering.model.cipher.splinewavelet;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.DataFormatException;
import org.amse.korovkin.ciphering.model.cipher.BlockCipher;
import org.amse.korovkin.ciphering.model.cipher.IKey;
import org.amse.korovkin.ciphering.model.cipher.util.Binary;
import org.amse.korovkin.ciphering.model.cipher.util.MathUtil;
import org.amse.korovkin.ciphering.model.cipher.util.TextWork;

import static org.amse.korovkin.ciphering.model.cipher.util.MathUtil.*;
import static org.amse.korovkin.ciphering.model.cipher.util.BinaryUtils.*;

/**
 *
 * @author Mikhail Korovkin
 */
public class Spline3WaveletCipher implements BlockCipher{

    private int BLOCK_SIZE = 32;
    private final int N = 256;
    private final Binary MODULE = new Binary(283);

    private final String name = "Spline3Wavelet";
    private static BlockCipher cipher = null;

    private Spline3WaveletCipher() {
    }
    public static BlockCipher getInstance() {
        if (cipher == null) {
            cipher = new Spline3WaveletCipher();
        }
        return cipher;
    }

    public String getName() {
        return this.name;
    }

    public int getBlockSize() {
        return BLOCK_SIZE;
    }

    public void setBlockSize(int blockSize) {
        this.BLOCK_SIZE = blockSize;
    }

    public void encrypt(String inputFile, String outputFile, String keyFile)
            throws FileNotFoundException, IOException{
        SplineKey key = new SplineKey();
        inputFile = TextWork.cutComma(inputFile);
        outputFile = TextWork.cutComma(outputFile);
        key.readFromFile(keyFile);
        encrypt(inputFile, outputFile, key);
    }

    public void encrypt(String inputFile, String outputFile, IKey key)
            throws FileNotFoundException, IOException{
        SplineKey spKey = (SplineKey) key;
        List<Integer> plainText = TextWork.readText(inputFile);
        List<Integer> encryptedText = new ArrayList<Integer>();
        List<List<Integer>> blocks = TextWork.getBlocks(plainText, BLOCK_SIZE);
        int K = spKey.getRounds();
        for (int j = 0; j < blocks.size(); j++) {
            SplineKey k = spKey.clone();
            List<Integer> wavelet = new ArrayList<Integer>();
            List<Integer> block = blocks.get(j);
//            System.out.println("block: " + j + "\t" + block);
            for (int i = 0; i < K; i++) {

                if (i == K - 1) {
//                    System.out.println("aasd");
                }
                int num = k.nextNumber();
                int e = k.removeElem(num);

                Binary c_4 = new Binary(block.get(modul(num-4, block.size())));
                Binary c_3 = new Binary(block.get(modul(num-3, block.size())));
                Binary c_2 = new Binary(block.get(modul(num-2, block.size())));
                Binary c_1 = new Binary(block.get(modul(num, block.size())));

                Binary eBin = new Binary(e);
                Binary A = eBin.sub(new Binary(k.getElem(num)));
                Binary B = eBin.sub(new Binary(k.getElem(num-1)));
                Binary C = eBin.sub(new Binary(k.getElem(num-2)));
                Binary D = eBin.sub(new Binary(k.getElem(num-3)));
                Binary E = eBin.sub(new Binary(k.getElem(num+1)));
                Binary F = new Binary(k.getElem(num+2)).sub(new Binary(k.getElem(num-1)));

                Binary Div_D = getInverse(D, MODULE);
                Binary Div_CD = getInverse(C.mult(D).mod(MODULE), MODULE);
                Binary Div_C = getInverse(C, MODULE);
                Binary Div_F = getInverse(F, MODULE);
                int new_c_3 = A.mult(Div_D).mult(c_4.sub(c_3)).sub(c_3).mod(MODULE).intValue();
                int new_c_2 = A.mult(E).mult(Div_CD).mult(c_4.sub(c_3))
                        .add(E.mult(Div_C).mult(c_3.sub(c_2))).add(c_2).mod(MODULE).intValue();

                block.set(modul(num - 2, block.size()), new_c_2);
                block.set(modul(num - 3, block.size()), new_c_3);

                Binary cRem = new Binary(block.remove(modul(num - 1, block.size())));
                Binary c2Bin = new Binary(new_c_2);
                int b = cRem.sub(c2Bin).add(B.mult(Div_F).mult(c2Bin.sub(c_1))).mod(MODULE).intValue();
                wavelet.add(b);

                // temporary for monitoring
                if (i == K - 1) {
                    //System.out.println(cCur + " - " + (xNext-xCur)*div*(cPrev) + " - " + (xCur-e)*div*(cNext));
//                    System.out.println("A: " + A + "\tB: " + B + "\tC:" + C
//                            + "\tD:" + D + "\tE:" + E + "\tF:" + F + "\tnum:" + num + "\te:" + e
//                            + "\tc_1:" + c_1 + "\tc_2:" + c_2 + "\tc_3:" + c_3 + "\tc_4:" + c_4
//                            + "\tcRem:" + cRem);
                }
                if (i != K - 1) {
                    block = shiftRight(block);
                }
//                System.out.println(" Result of round №" + i + ": b: " + block + " w: " + wavelet);
            }
//            System.out.println("block: " + j + "\t" + Calc.modArray(block, 256));
            block.addAll(wavelet);
            encryptedText.addAll(block);
        }
        TextWork.writeText(outputFile, encryptedText);
    }

    public void decrypt(String inputFile, String outputFile, String keyFile)
            throws FileNotFoundException, IOException{
        SplineKey key = new SplineKey();
        inputFile = TextWork.cutComma(inputFile);
        outputFile = TextWork.cutComma(outputFile);
        key.readFromFile(keyFile);
        decrypt(inputFile, outputFile, key);
    }
    public void decrypt(String inputFile, String outputFile, IKey key)
            throws FileNotFoundException, IOException {
        SplineKey spKey = (SplineKey)key;
        List<Integer> encrText = TextWork.readText(inputFile);
        List<List<Integer>> blocks = TextWork.getBlocks(encrText, BLOCK_SIZE);
        List<Integer> decryptedText = new ArrayList<Integer>();
        int K = spKey.getRounds();
        for (int j = 0; j < blocks.size(); j++) {
            SplineKey k = spKey.clone();
            List<Integer> block = blocks.get(j);
            List<Integer> wavelet = new ArrayList<Integer>();
            List<Integer> numbers = new ArrayList<Integer>();
            List<Integer> striking = new ArrayList<Integer>();

            int blockSize = block.size();
            for (int i = 0; i < K; i++) {
                wavelet.add(block.remove(blockSize-K));
                numbers.add(k.nextNumber());
                striking.add(k.removeElem(numbers.get(numbers.size()-1)));
            }
            for (int i = 0; i < K; i++) {

                if (i != 0) {
                    block = MathUtil.shiftLeft(block);
                }
                int num = numbers.get(K-i-1);
                int e = striking.get(K-i-1);

                Binary eBin = new Binary(e);
                Binary A = eBin.sub(new Binary(k.getElem(num)));
                Binary B = eBin.sub(new Binary(k.getElem(num-1)));
                Binary C = eBin.sub(new Binary(k.getElem(num-2)));
                Binary D = eBin.sub(new Binary(k.getElem(num-3)));
                Binary E = eBin.sub(new Binary(k.getElem(num+1)));
                Binary F = new Binary(k.getElem(num+2)).sub(new Binary(k.getElem(num-1)));

                block.add(MathUtil.modul(num - 1, block.size() + 1), 0);
                Binary c_1 = new Binary(block.get(modul(num, block.size())));
                Binary c_2 = new Binary(block.get(modul(num-2, block.size())));
                Binary c_3 = new Binary(block.get(modul(num-3, block.size())));
                Binary c_4 = new Binary(block.get(modul(num-4, block.size())));
                Binary b = new Binary(wavelet.get(K-i-1));
                
                Binary Div_F = getInverse(F, MODULE);
                Binary Div_C_E = getInverse(C.sub(E), MODULE);
                Binary Div_D_A = getInverse(D.sub(A), MODULE);
                
                int newC_1 = c_2.sub(B.mult(Div_F).mult(c_2.sub(c_1))).add(b).mod(MODULE).intValue();
                int newC_2 = C.mult(Div_C_E).mult(c_2).sub(E.mult(Div_C_E).mult(c_3)).mod(MODULE).intValue();
                int newC_3 = D.mult(Div_D_A).mult(c_3).sub(A.mult(Div_D_A).mult(c_4)).mod(MODULE).intValue();
                block.set(modul(num - 3, block.size()), newC_3);
                block.set(modul(num - 2, block.size()), newC_2);
                block.set(modul(num - 1, block.size()), newC_1);
//                     temporary for monitoring
//                System.out.println(" Result of round №" + i + ": b: " + block + " w: " + wavelet);
//                System.out.println("A: " + A + "\tB: " + B + "\tC:" + C
//                        + "\tD:" + D + "\tE:" + E + "\tF:" + F + "\tnum:" + num + "\te:" + e
//                        + "\tc_1:" + c_1 + "\tc_2:" + c_2 + "\tc_3:" + c_3 + "\tc_4:" + c_4);

                k.addElem(num, e);
            }
//            System.out.println("block: " + j + "\t" + block);
            decryptedText.addAll(block);
        }
        TextWork.writeText(outputFile, decryptedText);
    }

    public void attack(String inFile, String outFile)
            throws IOException, DataFormatException, InterruptedException {
        throw new UnsupportedOperationException("Not supported yet.");
    }
}